#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
#  Copyright (C) 2011 - Intrinsec
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Authors:
# Loic Michaux - loic.michaux@intrinsec.com
# Cyrille Barthelemy - cyrille.barthelemy@intrinsec.com

import httplib
import os
import sys
import getopt
from urlparse import urlparse
import xmlrpclib
import xml
import socket
import re
import time
import random

debug=False
vernum="0.4"
verbose=False


class ProxiedTransport(xmlrpclib.Transport):
    def __init__(self, proxy):
        self.set_proxy(proxy)
        self._use_datetime = True

    def set_proxy(self, proxy):
        self.proxy = proxy

    def make_connection(self, host):
        self.realhost = host
        h = httplib.HTTP(self.proxy)
        return h

    def send_request(self, connection, handler, request_body):
        connection.putrequest('POST', 'http://%s%s' % (self.realhost, handler))

    def send_host(self, connection, host):
        connection.putheader('Host', self.realhost)


def usage():
    """
    This function prints the posible options of this program.
    """
    print "+----------------------------------------------------------------------+"
    print "| Intrinsec-XMLRPC-Scanner Version "+ vernum +"                                 |"
    print "|                                                                      |"
    print "| This program is used to perform :                                    |"
    print "|  * discovery against XML-RPC services and related materials          |"
    print "|  * bruteforce against authenticated methods                          |"
    print "|                                                                      |"
    print "| Authors:                                                             |"
    print "|  Loic Michaux - loic.michaux@intrinsec.com                           |"
    print "|  Cyrille Barthelemy - cyrille.barthelemy@intrinsec.com               |"
    print "| Intrinsec - www.intrinsec.com                                        |"
    print "|                                                                      |"
    print "| XML RPC entry point is regularly http://<host>/xmlrpc.<ext>          |"
    print "| Drupal also provide XML-RPC server through the \"Service\" module    |"
    print "| This module is binded to an endpoint (eg. /?q=<endpoint>) to identify|"
    print "|                                                                      |"
    print "| Authenticated test are not yet implemented.                          |"
    print "+----------------------------------------------------------------------+"
    print
    print "\nUsage: %s <options>" % sys.argv[0]
    print "Options:"
    print "  -h, --help                           Show this help message and exit."
    print "  -U, --url                            URL to the XMLRPC entry point (*required*)."
    print "  --proxy                              Proxy URL."
    print "  -F, --framework                      Name of framework if known (wordpress,drupal)."
    print "  -u, --user                           Username to use for authenticated tests." 
    print "  -p, --pass                           Password to use for authenticated tests."
    print "  -L, --logins                         Dictionnary of username to test (activates bruteforce testing and requires passwords)." 
    print "  -P, --passwords                      Dictionnary of password to test (activates bruteforce testing and requires logins)."
    print "  -v, --verbose                        Verbose."
    print " "
    print "Example: "+sys.argv[0] +" -u http://www.xxxx.com/xmlrpc.php "
    print
    sys.exit(1)


def main():
    arg_ProxyURL=""
    arg_XMLRPC_URL=""
    arg_Framework=""
    arg_Login=""
    arg_Password=""
    arg_LoginDict=""
    arg_PassDict=""
    bruteforceEnabled=False
    authenticatedEnable=False

    print "Intrinsec - XML RPC Scanner - " + vernum + " - www.intrinsec.com"
    print "-----------------------------------------------------"
    print ""

    try:
        global debug
        global verbose

        options,remain = getopt.getopt(sys.argv[1:], "hU:F:u:p:L:P:v", ["help","url=", "proxy=", "framework=", "user=", "pass=", "logins=", "passwords=", "verbose"])

    except getopt.GetoptError:  usage()

    if (len(sys.argv) <= 1):
      usage()

    # Option parsing
    for opt, arg in options:
        if opt in ('-U', '--url'):
            arg_XMLRPC_URL = arg
        elif opt in ('','--proxy'):
            arg_ProxyURL = arg
        elif opt in ('-F', '--framework'):
            arg_Framework = arg
        elif opt in ('-u', '--user'):
            arg_Login = arg
        elif opt in ('-p', '--pass'):
            arg_Password = arg
        elif opt in ('-L', '--logins'):
            arg_LoginDict = arg
        elif opt in ('-P', '--passwords'):
            arg_PassDict = arg
        elif opt in ('-v', '--verbose'):
            verbose = True
        elif opt in ('-h', '--help'):
            usage()


    if arg_XMLRPC_URL == "":
        usage()

    if ((arg_LoginDict != "") or (arg_PassDict != "")):
        if ((arg_LoginDict == "") or (arg_PassDict == "")):
            print "[**] Bruteforce : Please provide Login AND Password dictionaries.\n\n"
            usage()
        elif not ((re.search("^(wordpress|drupal)$", arg_Framework, re.M|re.I))):
            print "[**] Framework not specificed (-F) or unimplemented (Wordpress/Drupal only)."
            usage()
        else:
            bruteforceEnabled = True
            f_logins = open(arg_LoginDict, 'r')
            f_pass = open(arg_PassDict, 'r')

    if ((arg_Login != "") or (arg_Password != "")):
        if ((arg_Login == "") or (arg_Password == "")):
            print "[**] Authenticated test : Please provide Login AND Password.\n\n"
            usage()
        else:
            authenticatedEnabled = True




    # XMLRPC Transport Init
    p = None


    if 'http_proxy' in os.environ:
        prox = urlparse(os.environ['http_proxy'])
        proxstr = ""
        if prox.username != None:
            proxstr += prox.username + ":"
        if prox.password != None:
            proxstr += prox.password + "@"
        proxstr += prox.netloc
        p = ProxiedTransport(prox.netloc)
    elif arg_ProxyURL != "":
        p = ProxiedTransport(arg_ProxyURL)

    # XMLRPC Connexion Init
    if p != None:
        server = xmlrpclib.Server(arg_XMLRPC_URL, transport=p)
    else:
        server = xmlrpclib.Server(arg_XMLRPC_URL)


    try:
        """ 
        # Step 1 : Enabled Methods Detection - generic
        """

        methods = server.system.listMethods()
        verboseMethods = {}

        if 'system.getCapabilities' in methods:
            print("[**] Server capabilities :\n%s\n" % server.system.getCapabilities())
        else:
            print("[!!] Server capabilities not available.")


        print("[**] %i methods are enabled on '%s' :\n" % (len(methods), arg_XMLRPC_URL))
        for m in methods:
            verboseMethods[m] = " * " + m


        if verbose:
            if 'system.methodSignature' in methods:
                for m in methods:
                    try :
                        method_usage = server.system.methodSignature(m)
                        if method_usage != "":
                            verboseMethods[m] += " ( " + str(method_usage) + " )"
                    except :
                        method_usage = ""

                        if 'system.methodHelp' in methods:
                            for m in methods:
                                try :
                                    about = server.system.methodHelp(m)
                                    if about != "":
                                        verboseMethods[m] += " : " + about
                                except :
                                    about = ""
                        
        for m in methods:
            print verboseMethods[m]

        """
        # Step 2 : Check for XML RPC Log file
        # Wordpress has a debug mode (must be enabled manually in xmlrpc.php) which will log all XML-RPC request
        # with parameters in a file named xmlrpc.log
        """
        print "[**] Checking for XML RPC Log file (contains methods call log with parameters)"
        xmlrpclog_found=False
        urlchunk = arg_XMLRPC_URL.split('/')
        basename = '/'.join(urlchunk[2:3])

        i = 1
        while i <= (len(urlchunk) - 3):
            dirname = '/'.join(urlchunk[3:len(urlchunk)-i])
            if verbose:
                print "Testing http://" + basename + '/' + dirname + '/xmlrpc.log'
            conn = httplib.HTTPConnection(basename)
            conn.request("GET", '/' + dirname + '/xmlrpc.log')
            res = conn.getresponse()
            if res.status == 200:
                data = res.read()
                print " -- Got a response for : http://" + basename +  '/' + dirname + '/xmlrpc.log'
                if re.search("^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d",data, re.M):
                    filename = time.strftime("%Y%m%d_%H%M%S", time.localtime()) + "_" + basename.replace('/','_') + '_' + dirname.replace('/','_') + 'xmlrpc.log'
                    print " => Seems to be a Wordpress XML RPC log file : outputting to ./" + filename
                    f = open(filename, "w")
                    f.write(data)
                    f.close
                    xmlrpclog_found = True
            conn.close()
            i = i +1

        if not xmlrpclog_found:
            print "[**] No XML RPC log found."





        """
        # Step 3: Bruteforce through web services
        """
        if bruteforceEnabled:
            print "[**] Bruteforce through XMLRPC starting (no output => no results)"
            for cur_login in f_logins.readlines():
                cur_login = cur_login.strip()
                f_pass.seek(0)
                for cur_pass in f_pass.readlines():
                    cur_pass = cur_pass.strip()

                    if (re.search("^wordpress$", arg_Framework, re.M | re.I)):
                        try:
                            result = server.metaWeblog.getRecentPosts(1, cur_login, cur_pass, 2)
                            if re.search("^\[\{'", "%s" % result, re.M):
                                print "  => Valid combination: " + cur_login + " : " + cur_pass
                        except xmlrpclib.Fault as error:
                            if re.search("XML-RPC services are disabled on this site", "%s" % error):
                                print "  [!!] %s " % error
                            elif not re.search("Bad login/pass combination.", "%s" % error, re.M):
                                print "  [!!] Unknown pattern - please send debug to cby@intrinsec.com" 


                    elif (re.search("^drupal$", arg_Framework, re.M | re.I)):
                        if 'blogger.getUserInfo' in methods:
                            try:
                                result = server.blogger.getUserInfo("TestKey", cur_login, cur_pass)
                                print "  => Valid combination: " + cur_login + " : " + cur_pass
                            except xmlrpclib.Fault as error:
                                if not re.search("You do not have permission to edit this blog", "%s" % error):
                                    print "  [!!] Unknown pattern - please send debug to cby@intrinsec.com : %s" % error
                        else:
                            print "[!!] Did not found any knonwn authenticated method to bruteforce - review method list and adapt module if necessary"
                 

            f_logins.close()
            f_pass.close()


        print "\n[--] Tests finished.\n"

    except IOError, error:
        print "[!!] Cannot open : %s" % error
    except socket.error, error:
        print "[!!] Network error: %s" % error
    except xml.parsers.expat.ExpatError, error:
        print "[!!] XML parsing error: %s" % error
    except xmlrpclib.ProtocolError, error:
        print "[!!] XML protocol error: %s" % error

if __name__ == "__main__":
    main()
